String.replaceAll(regex, replacement)를 직접 호출하면 매 호출 시마다 내부적으로 Pattern.compile(regex)가 실행되어 CPU 자원을 소모하고 임시 객체가 대량 생성된다. 반드시 static final Pattern으로 사전 컴파일해야 한다.
// SensitiveDataMasker.javapublic final class SensitiveDataMasker { // 16자리 카드 번호 마스킹: 앞 4자리, 뒤 4자리만 보존 private static final Pattern CARD_PATTERN = Pattern.compile( "\b(\d{4})[- ]?(\d{4})[- ]?(\d{4})[- ]?(\d{4})\b" ); // 주민등록번호: 앞 6자리 보존, 뒤 7자리 마스킹 private static final Pattern RRN_PATTERN = Pattern.compile( "\b(\d{6})[- ]?([1-4]\d{6})\b" ); // 전화번호: 중간 4자리 마스킹 private static final Pattern PHONE_PATTERN = Pattern.compile( "\b(01[016789])[- ]?(\d{3,4})[- ]?(\d{4})\b" ); public static String mask(String content) { if (content == null || content.isEmpty()) return content; String result = CARD_PATTERN.matcher(content).replaceAll("$1-****-****-$4"); result = RRN_PATTERN.matcher(result).replaceAll("$1-*******"); result = PHONE_PATTERN.matcher(result).replaceAll("$1-****-$3"); return result; }}
3. 대규모 로그 환경에서의 오류 핑거프린팅(Error Fingerprinting) 아키텍처
분산 시스템에서 DB 장애나 네트워크 타임아웃이 발생하면 1초에 수천 건의 동일한 에러 로그가 쏟아져 나온다. 단순 문자열 매칭으로 에러를 모니터링하면 다음과 같은 문제에 봉착한다.
[단순 에러 메시지 비교의 실패]
Log 1: "Connection timeout to 10.0.1.15:3306 at timestamp 1710000001"
Log 2: "Connection timeout to 10.0.1.18:3306 at timestamp 1710000002"
-> IP와 타임스탬프가 달라 서로 다른 2개의 에러로 인식되어 알림 폭탄 발생
flowchart TD
EX["발생한 예외 (Throwable)"] --> EXTRACT["스택 트레이스 프레임 추출"]
subgraph Filter["프레임워크 노이즈 필터링"]
F1["org.springframework.* 제거"]
F2["org.apache.catalina.* 제거"]
F3["org.hibernate.* 제거"]
F4["io.netty.* 제거"]
end
EXTRACT --> Filter
Filter --> SEED["핵심 비즈니스 시드 조합:<br/>1. 최상위 예외 클래스명<br/>2. 애플리케이션 첫 번째 스택 프레임 (클래스+메서드)<br/>3. Root Cause 예외 클래스명"]
SEED --> HASH["SHA-256 해시 연산"]
HASH --> FP["12자리 핑거프린트 도출 (예: 'a4f9b21c08d3')"]
3.1 ErrorFingerprinter 알고리즘 구현
// ErrorFingerprinter.javapublic final class ErrorFingerprinter { private static final Set<String> IGNORED_PACKAGES = Set.of( "org.springframework", "org.apache", "org.hibernate", "io.netty", "jdk.internal", "java.lang.reflect" ); public static String fingerprint(Throwable throwable) { if (throwable == null) return "none"; StringBuilder seed = new StringBuilder(); seed.append(throwable.getClass().getName()).append(";"); // 1. 프레임워크 스택을 제외한 첫 번째 순수 비즈니스 애플리케이션 프레임 검색 StackTraceElement appFrame = findAppFrame(throwable); if (appFrame != null) { seed.append(appFrame.getClassName()).append("#") .append(appFrame.getMethodName()).append(";"); } // 2. 근본 원인(Root Cause) 예외 클래스 추가 Throwable rootCause = getRootCause(throwable); seed.append(rootCause.getClass().getName()); // 3. SHA-256 해시 후 앞 12자리 추출 return sha256Hex(seed.toString()).substring(0, 12); }}
효과: 라인 번호가 배포로 인해 약간 바뀌거나 동적 파라미터가 달라져도, 코드의 논리적 결함 위치가 동일하다면 언제나 동일한 12자리 해시값(error_fingerprint)을 생성한다.
4. Grafana Loki 연동 및 N+1 쿼리 탐지 모니터링
4.1 Logfmt 구조화 로그 출력
생성된 메타데이터는 Grafana Loki에서 LogQL로 고속 인덱싱될 수 있도록 SLF4J Marker와 key=value 형태의 logfmt 규격으로 출력된다.